package cn.stylefeng.guns.modular.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.stylefeng.guns.core.context.constant.ConstantContext;
import cn.stylefeng.guns.core.context.login.LoginContextHolder;
import cn.stylefeng.guns.core.exception.ServiceException;
import cn.stylefeng.guns.modular.entity.Reserved;
import cn.stylefeng.guns.modular.entity.Room;
import cn.stylefeng.guns.modular.mapper.ReservedMapper;
import cn.stylefeng.guns.modular.myenum.PeriodUnitEnum;
import cn.stylefeng.guns.modular.myenum.ReservedStatusEnum;
import cn.stylefeng.guns.modular.param.ReservedAddParam;
import cn.stylefeng.guns.modular.service.ReservedService;
import cn.stylefeng.guns.modular.service.RoomsService;
import cn.stylefeng.guns.modular.vo.ReservedInfoVO;
import cn.stylefeng.guns.sys.modular.consts.entity.SysConfig;
import cn.stylefeng.guns.sys.modular.consts.service.SysConfigService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.util.*;

/**
 * @author 李宝嘉
 * @Date 2022/1/1 15:57
 */
@Service
public class ReservedServiceImpl extends ServiceImpl<ReservedMapper, Reserved> implements ReservedService {
    //二进制标志位最大位数
    public static final int MAX_BIT = 96;

    @Resource
    private SysConfigService sysConfigService;
    @Resource
    private RoomsService roomsService;
    @Resource
    private RedisTemplate<String, Object> redisTemplate;
    @Resource
    private DefaultRedisScript<Long> defaultRedisScript;

    /**
     * 增加一条预约记录
     *
     * @param reservedAddParam 预约信息
     */
    @Override
    @Transactional
    public void addReserved(ReservedAddParam reservedAddParam) {
        Long userId = LoginContextHolder.me().getSysLoginUserId();
        checkDateReserved(reservedAddParam);
        checkOverBreach(userId);
        checkExistReserved(userId, reservedAddParam);

        DateTime today = DateUtil.parse(reservedAddParam.getBeginTime());
        Reserved reserved = new Reserved();
        BeanUtil.copyProperties(reservedAddParam, reserved);
        reserved.setBeginTime(today);
        reserved.setUserId(userId);
        reserved.setStatus(ReservedStatusEnum.PENDING);
        reserved.setEndTime(DateUtil.offsetMinute(reserved.getBeginTime(),
                reserved.getDuration().intValue() * 60));
        this.save(reserved);
    }


    /**
     * 获取此用户今明两天的预约记录
     *
     * @return 预约信息列表
     */
    @Override
    public List<ReservedInfoVO> getListByUser() {
        Long userId = LoginContextHolder.me().getSysLoginUserId();
        DateTime today = DateUtil.beginOfDay(DateUtil.date());
        DateTime tomorrow = DateUtil.endOfDay(DateUtil.tomorrow());
        List<ReservedInfoVO> reservedInfoVOS = this.baseMapper.getListByUserIdAndTimePeriod(userId,
                today.toString(), tomorrow.toString());
        List<Room> roomList = roomsService.list();
        if (CollectionUtil.isNotEmpty(reservedInfoVOS)) {
            for (ReservedInfoVO vo :
                    reservedInfoVOS) {
                Room pRoom = roomList.stream().filter(room -> room.getId().equals(vo.getPid())).findFirst().get();
                vo.setRoomName(pRoom.getName() + "-" + vo.getRoomName());
            }
        }
        return reservedInfoVOS;
    }

    /**
     * 座位签到
     *
     * @param seatId     座位id
     * @param reservedId 预约id
     */
    @Override
    public void seatSignIn(Long seatId, Long reservedId) {
        //判断预约是否存在
        Long userId = LoginContextHolder.me().getSysLoginUserId();
        LambdaQueryWrapper<Reserved> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Reserved::getId, reservedId)
                .eq(Reserved::getSeatId, seatId).eq(Reserved::getUserId, userId);
        Reserved reserved = this.getOne(queryWrapper);
        if (ObjectUtil.isNull(reserved)) {
            throw new ServiceException(200034, "预约不存在");
        }
        //判断是否到签到时间
        long betweenMinute = DateUtil.between(new Date(), reserved.getBeginTime(), DateUnit.MINUTE);
        List<SysConfig> sysConfigs = sysConfigService.reservedList();
        SysConfig signInTime = sysConfigs.stream()
                .filter(sysConfig -> sysConfig.getCode().equals("SIGNIN_TIME"))
                .findFirst().get();
        if (betweenMinute > Long.parseLong(signInTime.getValue())) {
            throw new ServiceException(20035, "还没到签到时间");
        }
        //更新预约记录
        LambdaUpdateWrapper<Reserved> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.set(Reserved::getTakeTime, new Date())
                .set(Reserved::getStatus, ReservedStatusEnum.USE).eq(Reserved::getId, reserved.getId());
        this.update(updateWrapper);

    }

    /**
     * 获取指定日期的预约列表，只要待履约和使用中的
     *
     * @param beginTime 开始时间9-
     * @return 预约列表
     */
    @Override
    public List<Reserved> getListByBeginTime(String beginTime) {
        LambdaQueryWrapper<Reserved> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.apply("date_format(begin_time,'%Y-%m-%d')={0}", beginTime).le(Reserved::getStatus, ReservedStatusEnum.USE);
        return this.list(queryWrapper);
    }

    /**
     * 取消预约
     *
     * @param reservedId 预约id
     */
    @Override
    public void cancelReserved(Long reservedId) {
        Long userId = LoginContextHolder.me().getSysLoginUserId();
        this.checkMaxCancelNum(userId);
        LambdaUpdateWrapper<Reserved> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.set(Reserved::getStatus, ReservedStatusEnum.CANCEL)
                .eq(Reserved::getId, reservedId)
                .eq(Reserved::getUserId, userId);
        this.update(updateWrapper);
    }

    /**
     * 退座
     *
     * @param reservedId 预约id
     */
    @Override
    public void withdrawReserved(Long reservedId) {
        Long userId = LoginContextHolder.me().getSysLoginUserId();
        LambdaUpdateWrapper<Reserved> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.set(Reserved::getQuitTime, new Date())
                .set(Reserved::getStatus, ReservedStatusEnum.WITHDRAW)
                .eq(Reserved::getId, reservedId)
                .eq(Reserved::getUserId, userId);
        this.update(updateWrapper);
    }

    /**
     * 续约
     *
     * @param reservedId 预约id
     * @param seatId     座位id
     * @return 剩余时间存在的预约列表
     */
    @Override
    public List<Reserved> renewalReserved(Long reservedId, Long seatId) {
        Reserved reserved = this.getById(reservedId);
        float minute = reserved.getDuration() * 60;
        DateTime endTime = DateUtil.offsetMinute(reserved.getBeginTime(), (int) minute);
        List<Reserved> reservedList = this.baseMapper.getListByRemainingTime(endTime.toString(),
                DateUtil.endOfDay(endTime).toString(), reserved.getSeatId());
        checkRemainingTime(reservedList, endTime, seatId);
        return reservedList;
    }

    /**
     * 通过redis检查用户此时段是否存在预约、座位是否被他人预约。redis单线程，不存在多个线程抢占同一时段出现多个
     * 抢占成功。
     *
     * @param userId           用户id
     * @param reservedAddParam 预约信息
     */
    private void checkExistReserved(Long userId, ReservedAddParam reservedAddParam) {
        DateTime today = DateUtil.parse(reservedAddParam.getBeginTime());
        String todayStr = DateUtil.format(today, "MM-dd");
        ArrayList<String> keys = new ArrayList<>();
        keys.add("userId:" + todayStr + userId.toString());
        keys.add("seatId:" + todayStr + reservedAddParam.getSeatId().toString());
        String recordBit = beginTime2Bit(reservedAddParam);
        Long result = redisTemplate.execute(defaultRedisScript, keys, recordBit);
        if (result == 0) {
            throw new ServiceException(200031, "不可预约");
        }
    }

    /**
     * 将预约时间转换为二进制标志位，长度96
     *
     * @param reservedAddParam 预约信息
     * @return 二进制标志位字符串
     */
    private String beginTime2Bit(ReservedAddParam reservedAddParam) {
//        SysConfig timeUnit = sysConfigs.stream()
//                .filter(sysConfig -> sysConfig.getCode().equals("TIME_UNIT"))
//                .findFirst().get();
        int timeUnitInt = ConstantContext.me().getInt("TIME_UNIT");
        DateTime sourceBeginTime = DateUtil.parse(reservedAddParam.getBeginTime());
        int sourceBeginMinutes = sourceBeginTime.hour(true) * 60 + sourceBeginTime.minute();
        int beginIndex = sourceBeginMinutes / timeUnitInt;
        int endIndex = ((int) (reservedAddParam.getDuration() * 60 / timeUnitInt)) + beginIndex;
        StringBuilder oneStr = new StringBuilder();
        for (int i = 0; i < MAX_BIT; i++) {
            if (i >= beginIndex && i < endIndex) oneStr.append("1");
            else oneStr.append("0");
        }
        return oneStr.toString();
    }

    /**
     * 检查剩余时间是否可以预约
     *
     * @param reservedList 预约列表
     * @param startTime    剩余时间开始时间也是此次预约的结束时间
     */
    private void checkRemainingTime(List<Reserved> reservedList, DateTime startTime, Long seatId) {
        //判断此次预约的结束时间是房间关门时间，不能续约
        DateTime roomCloseTime = DateTime.of(this.roomsService.getCloseTimeBySeatId(seatId));
        if (roomCloseTime.hour(true) == startTime.hour(true)
                && roomCloseTime.minute() == startTime.minute()) {
            throw new ServiceException(200037, "不可续约");
        }
        //判断剩余预约时长是否等于此次预约距闭馆时间时长
        if (CollectionUtil.isNotEmpty(reservedList)) {
            double durationSum = reservedList.stream().mapToDouble(Reserved::getDuration).sum() * 60;
            DateTime todayRoomCloseTime = new DateTime(TimeZone.getDefault());
            todayRoomCloseTime.setField(DateField.HOUR_OF_DAY, roomCloseTime.hour(true));
            todayRoomCloseTime.setField(DateField.MINUTE, roomCloseTime.minute());
            long remainingMinute = DateUtil.between(startTime, todayRoomCloseTime, DateUnit.MINUTE);
            if (Long.valueOf((long) durationSum).equals(remainingMinute)) {
                throw new ServiceException(200037, "不可续约");
            }
        }
    }

    /**
     * 检验预约时间符合规则，防止恶意预约
     *
     * @param reservedAddParam 预约记录
     * @param isToday          是否预约今天
     */
    private void checkOverCloseDate(ReservedAddParam reservedAddParam, boolean isToday) {
        DateTime today = DateTime.now();
        DateTime tomorrow = DateUtil.tomorrow();
        Room room = this.roomsService.lambdaQuery().select(Room::getOpenDate, Room::getCloseDate)
                .eq(Room::getId, reservedAddParam.getRoomId()).one();
        DateTime roomOpenDate, roomCloseDate;
        roomCloseDate = DateUtil.date(room.getCloseDate());
        if (isToday) {//今天的预约
            roomOpenDate = today;
            this.fillDate(today, roomCloseDate);
        } else {//明天的预约
            roomOpenDate = DateUtil.date(room.getOpenDate());
            this.fillDate(tomorrow, roomOpenDate);
            this.fillDate(tomorrow, roomCloseDate);
        }
        //预约时长duration检验
        DateTime targetStartDate = DateUtil.parse(reservedAddParam.getBeginTime());
        int minute = (int) (reservedAddParam.getDuration() * 60);
        int timeUnit = Integer.parseInt(ConstantContext.me().getStr("TIME_UNIT"));
        int maxDuration = Integer.parseInt(ConstantContext.me().getStr("MAX_TIME"));
        if (minute == 0 || minute % timeUnit != 0 || minute > maxDuration) {
            throw new ServiceException(200037, "不可预约");
        }
        //预约开始时间分钟检验
        int targetStartMinute = targetStartDate.minute() == 0 ? 60 : targetStartDate.minute();
        if (targetStartMinute % timeUnit != 0) {
            throw new ServiceException(200037, "不可预约");
        }
        //预约时间段要在房间开闭馆时间段内检验
        DateTime targetEndDate = DateUtil.offsetMinute(targetStartDate, minute);
        if (!targetStartDate.isIn(roomOpenDate, roomCloseDate) ||
                !targetEndDate.isIn(roomOpenDate, roomCloseDate)) {
            throw new ServiceException(200037, "不可预约");
        }
    }

    /**
     * 将原日期的年月日填充到目标日期
     *
     * @param sourceDate 原日期
     * @param targetDate 目标日期
     */
    private void fillDate(DateTime sourceDate, DateTime targetDate) {
        targetDate.setField(DateField.YEAR, sourceDate.year());
        targetDate.setField(DateField.MONTH, sourceDate.month());
        targetDate.setField(DateField.DAY_OF_MONTH, sourceDate.dayOfMonth());
    }

    /**
     * 判断用户在此时间段是否存在预约
     *
     * @param userId           用户id
     * @param reservedAddParam 预约信息
     * @throws ServiceException 你在此时间段已有预约
     */
    private void checkUserExistReserved(Long userId, ReservedAddParam reservedAddParam) {
        LambdaQueryWrapper<Reserved> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Reserved::getUserId, userId);
        queryWrapper.apply("date_format(begin_time,'%Y-%m-%d')={0}",
                        reservedAddParam.getBeginTime().split(" ")[0])
                .lt(Reserved::getStatus, ReservedStatusEnum.USE);
        List<Reserved> reservedList = this.list(queryWrapper);
        boolean flag = this.checkTimeOverlap(reservedList, reservedAddParam);
        if (!flag) {
            throw new ServiceException(200031, "你在此时间段已有预约");
        }
    }

    /**
     * 判断座位在此时间段是否被预约
     *
     * @param reservedAddParam 预约信息
     * @throws ServiceException 座位在此时间段已被预约
     */
    private void checkSeatExistReserved(ReservedAddParam reservedAddParam) {
        LambdaQueryWrapper<Reserved> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Reserved::getSeatId, reservedAddParam.getSeatId());
        queryWrapper.apply("date_format(begin_time,'%Y-%m-%d')={0}",
                        reservedAddParam.getBeginTime().split(" ")[0])
                .lt(Reserved::getStatus, ReservedStatusEnum.USE);
        List<Reserved> reservedList = this.list(queryWrapper);
        boolean flag = this.checkTimeOverlap(reservedList, reservedAddParam);
        if (!flag) {
            throw new ServiceException(200032, "座位在此时间段已被预约");
        }
    }

    /**
     * 检查是否超过最大违约次数。根据配置一个周期可以是日、周、月、年。
     */
    private void checkOverBreach(Long userId) {
        List<SysConfig> sysConfigs = sysConfigService.reservedList();
        SysConfig periodUnit = sysConfigs.stream()
                .filter(sysConfig -> sysConfig.getCode().equals("PERIODUNIT"))
                .findFirst().get();
        SysConfig periodMaxNum = sysConfigs.stream()
                .filter(sysConfig -> sysConfig.getCode().equals("PERIODMAXNUM"))
                .findFirst().get();
        PeriodUnitEnum periodUnitEnum = PeriodUnitEnum.getByCode(Integer.parseInt(periodUnit.getValue()));
        Date today = new Date();
        Integer num;
        String start = "";
        String end = "";
        switch (periodUnitEnum) {
            case DAY:
                start = DateUtil.beginOfDay(today).toString();
                end = DateUtil.endOfDay(today).toString();
                break;
            case WEEK:
                start = DateUtil.beginOfWeek(today).toString();
                end = DateUtil.endOfWeek(today).toString();
                break;
            case MONTH:
                start = DateUtil.beginOfMonth(today).toString();
                end = DateUtil.endOfMonth(today).toString();
                break;
            case YEAR:
                start = DateUtil.beginOfYear(today).toString();
                end = DateUtil.endOfYear(today).toString();
                break;
        }
        num = this.baseMapper.countByTimePeriod(start, end, userId, ReservedStatusEnum.BREACH.getCode());
        System.out.println(num);
        if (Integer.parseInt(periodMaxNum.getValue()) < num) {
            throw new ServiceException(200033, "违约次数过多");
        }
    }

    /**
     * 预约今天还是明天的座位
     *
     * @param reservedAddParam 预约信息
     */
    public void checkDateReserved(ReservedAddParam reservedAddParam) {
        DateTime targetDate = DateUtil.parse(reservedAddParam.getBeginTime());
        String targetDateString = DateUtil.format(targetDate, "yyyy-MM-dd");

        //预约今天座位
        if (targetDateString.equals(DateUtil.today())) {
            this.checkOverCloseDate(reservedAddParam, true);
            //预约明天座位
        } else if (targetDateString.equals(DateUtil.format(DateUtil.tomorrow(), "yyyy-MM-dd"))) {
            //明天是否可以预约
            String closedDate = ConstantContext.me().getStr("CLOSED_DATE");
            if (!closedDate.equals("0")) {
                DateTime closeDate = DateUtil.beginOfDay(DateUtil.parse(closedDate));
                if (!targetDate.isBefore(closeDate)) {
                    throw new ServiceException(200037, "不可续约");
                }
            }
            //没到明天开始预约时间不能预约
            String[] reservedTime = ConstantContext.me().getStr("RESERVED_TIME").split(":");
            DateTime reservedDate = DateTime.now();
            reservedDate.setField(DateField.HOUR_OF_DAY, Integer.parseInt(reservedTime[0]));
            reservedDate.setField(DateField.MINUTE, Integer.parseInt(reservedTime[1]));

            if (DateTime.of(Calendar.getInstance()).isBefore(reservedDate)) {
                throw new ServiceException(200037, "不可续约");
            }
            this.checkOverCloseDate(reservedAddParam, false);
        } else {
            throw new ServiceException(200037, "不可续约");
        }
    }

    /**
     * 判断时间段是否重叠
     *
     * @param reservedList     预约列表
     * @param reservedAddParam 预约信息
     * @return 真值表示存在重叠
     */
    private boolean checkTimeOverlap(List<Reserved> reservedList, ReservedAddParam reservedAddParam) {
        boolean flag = true;
        if (CollectionUtil.isNotEmpty(reservedList)) {
            DateTime sourceBeginTime = DateUtil.parse(reservedAddParam.getBeginTime());
            int sourceBeginMinutes = sourceBeginTime.hour(true) * 60 + sourceBeginTime.minute();
            int sourceEndMinutes = sourceBeginMinutes + (int) (reservedAddParam.getDuration() * 60);
            for (Reserved reserved :
                    reservedList) {
                DateTime startTime = DateUtil.date(reserved.getBeginTime());
                int startMinutes = startTime.hour(true) * 60 + startTime.minute();
                int endMinutes = startMinutes + (int) (reserved.getDuration() * 60);
                if ((sourceBeginMinutes > startMinutes && sourceBeginMinutes < endMinutes) ||
                        (sourceEndMinutes > startMinutes && sourceEndMinutes < endMinutes) ||
                        (sourceBeginMinutes <= startMinutes && sourceEndMinutes >= endMinutes)) {
                    flag = false;
                }
            }
        }
        System.out.println(flag);
        return flag;
    }

    /**
     * 判断是否超过今日最大取消次数
     *
     * @param userId 用户id
     */
    private void checkMaxCancelNum(Long userId) {
        List<SysConfig> sysConfigs = sysConfigService.reservedList();
        SysConfig maxCancelNum = sysConfigs.stream()
                .filter(sysConfig -> sysConfig.getCode().equals("MAX_CANCEL_NUM"))
                .findFirst().get();
        Date today = new Date();
        Integer num;
        String start = "";
        String end = "";
        start = DateUtil.beginOfDay(today).toString();
        end = DateUtil.endOfDay(today).toString();
        num = this.baseMapper.countByTimePeriod(start, end, userId, ReservedStatusEnum.CANCEL.getCode());
        if (num > Integer.parseInt(maxCancelNum.getValue())) {
            throw new ServiceException(200036, "取消次数过多");
        }
    }
}
